﻿/*
Copyright © 2010 François Karman

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

See <license.txt> for the complete license of the software.
*/

using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Faz.Dependencies.Controls
{
    /// <summary>
    /// Represents a link between two <see cref="FrameworkElement"/>.
    /// </summary>
    public class LinkViewConnector : Control
    {
        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.Source"/> property.
        /// </summary>
        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(FrameworkElement), typeof(LinkViewConnector));

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.Destination"/> property.
        /// </summary>
        public static readonly DependencyProperty DestinationProperty =
            DependencyProperty.Register("Destination", typeof(FrameworkElement), typeof(LinkViewConnector));

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.Geometry"/> property.
        /// </summary>
        public static readonly DependencyProperty GeometryProperty =
            DependencyProperty.Register("Geometry", typeof(Geometry), typeof(LinkViewConnector));

        public static readonly DependencyProperty LinkViewProperty =
            DependencyProperty.Register("LinkView", typeof(LinkView), typeof(LinkViewConnector));

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.StartPoint"/> read-only property.
        /// </summary>
        public static readonly DependencyProperty StartPointProperty;

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.EndPoint"/> read-only property.
        /// </summary>
        public static readonly DependencyProperty EndPointProperty;

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.StartPointAngle"/> read-only property.
        /// </summary>
        public static readonly DependencyProperty StartPointAngleProperty;

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.EndPointAngle"/> read-only property.
        /// </summary>
        public static readonly DependencyProperty EndPointAngleProperty;

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.StartPoint"/> read-only property.
        /// </summary>
        private static readonly DependencyPropertyKey StartPointPropertyKey;

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.EndPoint"/> read-only property.
        /// </summary>
        private static readonly DependencyPropertyKey EndPointPropertyKey;

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.StartPointAngle"/> read-only property.
        /// </summary>
        private static readonly DependencyPropertyKey StartPointAnglePropertyKey;

        /// <summary>
        /// Identifies the <see cref="LinkViewConnector.EndPointAngle"/> read-only property.
        /// </summary>
        private static readonly DependencyPropertyKey EndPointAnglePropertyKey;

        /// <summary>
        /// The current position and size of the source element.
        /// </summary>
        private LinkViewRect sourceRect;

        /// <summary>
        /// The current position and size of the destination element.
        /// </summary>
        private LinkViewRect destinationRect;

        /// <summary>
        /// Initializes static members of the LinkViewConnector class.
        /// </summary>
        [SuppressMessage(
            "Microsoft.Performance",
            "CA1810:InitializeReferenceTypeStaticFieldsInline",
            Justification = "This static constructor is required for WPF initialization")]
        static LinkViewConnector()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(LinkViewConnector),
                new FrameworkPropertyMetadata(typeof(LinkViewConnector)));

            // Initialize the read-only properties
            StartPointPropertyKey = DependencyProperty.RegisterReadOnly("StartPoint", typeof(Point), typeof(LinkViewConnector), new PropertyMetadata());
            StartPointProperty = StartPointPropertyKey.DependencyProperty;

            StartPointAnglePropertyKey = DependencyProperty.RegisterReadOnly("StartPointAngle", typeof(double), typeof(LinkViewConnector), new PropertyMetadata());
            StartPointAngleProperty = StartPointAnglePropertyKey.DependencyProperty;

            EndPointPropertyKey = DependencyProperty.RegisterReadOnly("EndPoint", typeof(Point), typeof(LinkViewConnector), new PropertyMetadata());
            EndPointProperty = EndPointPropertyKey.DependencyProperty;

            EndPointAnglePropertyKey = DependencyProperty.RegisterReadOnly("EndPointAngle", typeof(double), typeof(LinkViewConnector), new PropertyMetadata());
            EndPointAngleProperty = EndPointAnglePropertyKey.DependencyProperty;
        }

        /// <summary>
        /// Initializes a new instance of the LinkViewConnector class.
        /// </summary>
        public LinkViewConnector()
        {
        }

        /// <summary>
        /// Gets or sets the source of the connector.
        /// </summary>
        public FrameworkElement Source
        {
            get { return (FrameworkElement)this.GetValue(SourceProperty); }
            set { this.SetValue(SourceProperty, value); }
        }

        /// <summary>
        /// Gets or sets the destination of the connector.
        /// </summary>
        public FrameworkElement Destination
        {
            get { return (FrameworkElement)this.GetValue(DestinationProperty); }
            set { this.SetValue(DestinationProperty, value); }
        }

        /// <summary>
        /// Gets the geometry association with the connector.
        /// </summary>
        public Geometry Geometry
        {
            get { return (Geometry)this.GetValue(GeometryProperty); }
            private set { this.SetValue(GeometryProperty, value); }
        }

        public LinkView LinkView
        {
            get { return (LinkView)this.GetValue(LinkViewProperty); }
            set { this.SetValue(LinkViewProperty, value); }
        }

        /// <summary>
        /// Gets the position of the start point.
        /// </summary>
        public Point StartPoint
        {
            get { return (Point)this.GetValue(StartPointProperty); }
            private set { this.SetValue(StartPointPropertyKey, value); }
        }

        /// <summary>
        /// Gets the angle of the start point.
        /// </summary>
        public double StartPointAngle
        {
            get { return (double)this.GetValue(StartPointAngleProperty); }
            private set { this.SetValue(StartPointAnglePropertyKey, value); }
        }

        /// <summary>
        /// Gets the position of the end point.
        /// </summary>
        public Point EndPoint
        {
            get { return (Point)this.GetValue(EndPointProperty); }
            private set { this.SetValue(EndPointPropertyKey, value); }
        }

        /// <summary>
        /// Gets the angle of the end point.
        /// </summary>
        public double EndPointAngle
        {
            get { return (double)this.GetValue(EndPointAngleProperty); }
            private set { this.SetValue(EndPointAnglePropertyKey, value); }
        }

        /// <summary>
        /// Computes the line coordinates that links two elements.
        /// </summary>
        /// <param name="source">The source of the link.</param>
        /// <param name="destination">The destination of the link.</param>
        /// <returns>The path of the line that link the two elements.</returns>
        /// <remarks>
        /// This method asserts that there is no intersection between the two elements.
        /// </remarks>
        public static PathGeometry CreateGeometry(LinkViewRect source, LinkViewRect destination)
        {
            // compute the difference between the centers
            double diffY = destination.Center.Y - source.Center.Y;
            double diffX = destination.Center.X - source.Center.X;

            // Contain the start and end point of the line
            Point[] points;
            if (diffX == 0.0)
            {
                if (diffY > 0.0)
                {
                    Point start = new Point(source.Center.X, source.Center.Y + (source.Size.Height / 2));
                    Point end = new Point(destination.Center.X, destination.Center.Y - (destination.Size.Height / 2));
                    points = new Point[] { start, end };
                }
                else
                {
                    Point start = new Point(source.Center.X, source.Center.Y - (source.Size.Height / 2));
                    Point end = new Point(destination.Center.X, destination.Center.Y + (destination.Size.Height / 2));
                    points = new Point[] { start, end };
                }
            }
            else if (diffY == 0.0)
            {
                if (diffX > 0.0)
                {
                    Point start = new Point(source.Center.X + (source.Size.Width / 2), source.Center.Y);
                    Point end = new Point(destination.Center.X - (destination.Size.Width / 2), destination.Center.Y);
                    points = new Point[] { start, end };
                }
                else
                {
                    Point start = new Point(source.Center.X - (source.Size.Width / 2), source.Center.Y);
                    Point end = new Point(destination.Center.X + (destination.Size.Width / 2), destination.Center.Y);
                    points = new Point[] { start, end };
                }
            }
            else
            {
                // The coef of the line
                double coef = Math.Abs(diffY / diffX);

                // The adjustments on the source
                double sourceDX;
                double sourceDY;
                if (source.Size.Height / source.Size.Width < coef)
                {
                    sourceDY = source.Size.Height / 2;
                    sourceDX = sourceDY / coef;
                }
                else
                {
                    sourceDX = source.Size.Width / 2;
                    sourceDY = sourceDX * coef;
                }

                // The adjustments on the destination
                double destinationDX;
                double destinationDY;
                if (destination.Size.Height / destination.Size.Width < coef)
                {
                    destinationDY = destination.Size.Height / 2;
                    destinationDX = destinationDY / coef;
                }
                else
                {
                    destinationDX = destination.Size.Width / 2;
                    destinationDY = destinationDX * coef;
                }

                // Compute the final points of the line
                double startX;
                double startY;
                double endX;
                double endY;
                if (diffX > 0.0)
                {
                    startX = source.Center.X + sourceDX;
                    endX = destination.Center.X - destinationDX;
                }
                else
                {
                    startX = source.Center.X - sourceDX;
                    endX = destination.Center.X + destinationDX;
                }

                if (diffY > 0.0)
                {
                    startY = source.Center.Y + sourceDY;
                    endY = destination.Center.Y - destinationDY;
                }
                else
                {
                    startY = source.Center.Y - sourceDY;
                    endY = destination.Center.Y + destinationDY;
                }

                points = new Point[] { new Point(startX, startY), new Point(endX, endY) };
            }

            // Create the path
            PathGeometry geometry = new PathGeometry();
            PathFigure figure = new PathFigure();
            figure.StartPoint = points[0];
            figure.Segments.Add(new LineSegment(points[1], true));
            geometry.Figures.Add(figure);
            return geometry;
        }

        /// <summary>
        /// Called when one of the <see cref="LinkViewConnector.Source"/> or
        /// <see cref="LinkViewConnector.Destination"/> property is updated.
        /// </summary>
        /// <param name="e">
        /// The arguments of the event containing the old and the new values of the property.
        /// </param>
        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (e.Property == SourceProperty || e.Property == DestinationProperty)
            {
                // Remove the event handler - if any
                if (e.OldValue != null)
                {
                    ((UIElement)e.OldValue).LayoutUpdated -= this.OnElementLayoutUpdated;
                }

                // Add the event handler
                if (e.NewValue != null)
                {
                    ((UIElement)e.NewValue).LayoutUpdated += this.OnElementLayoutUpdated;
                }
            }

            base.OnPropertyChanged(e);
        }

        /// <summary>
        /// Called when one of the <see cref="LinkViewConnector.Source"/> or
        /// <see cref="LinkViewConnector.Destination"/> property has its layout updated.
        /// </summary>
        /// <param name="sender">The parameter is not used.</param>
        /// <param name="e">The parameter is not used.</param>
        private void OnElementLayoutUpdated(object sender, EventArgs e)
        {
            LinkViewRect sourcePosition = this.ExtractRectangle(this.Source);
            LinkViewRect destinationPosition = this.ExtractRectangle(this.Destination);

            if (sourcePosition != this.sourceRect || destinationPosition != this.destinationRect)
            {
                this.sourceRect = sourcePosition;
                this.destinationRect = destinationPosition;

                PathGeometry geometry = CreateGeometry(this.sourceRect, this.destinationRect);
                this.Geometry = geometry;

                // Extract the start and end points position from the geometry
                Point startPoint;
                Point startPointTangent;
                Point endPoint;
                Point endPointTangent;
                geometry.GetPointAtFractionLength(0.0, out startPoint, out startPointTangent);
                geometry.GetPointAtFractionLength(1.0, out endPoint, out endPointTangent);

                this.StartPoint = startPoint;
                this.EndPoint = endPoint;
                this.StartPointAngle = Math.Atan2(-startPointTangent.Y, -startPointTangent.X) * (180 / Math.PI);
                this.EndPointAngle = Math.Atan2(endPointTangent.Y, endPointTangent.X) * (180 / Math.PI);
            }
        }

        /// <summary>
        /// Extracts the position and size of a specific element.
        /// </summary>
        /// <param name="element">The reference element.</param>
        /// <returns>
        /// The position and size of the element relative to the current instance.
        /// </returns>
        private LinkViewRect ExtractRectangle(FrameworkElement element)
        {
            Size size = element.RenderSize;
            Point center = element.TranslatePoint(new Point(size.Width / 2.0, size.Height / 2.0), this);
            return new LinkViewRect(center, size);
        }
    }
}
